package org.p2psimulator.entity.protocol.unstructured.p2prep;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import java.util.logging.Level;
import java.util.logging.Logger;
import org.p2psimulator.entity.Id;
import org.p2psimulator.entity.Message;
import org.p2psimulator.entity.Peer;
import org.p2psimulator.entity.behavior.BehaviorTypes;
import org.p2psimulator.entity.id.DoubleId;
import org.p2psimulator.entity.protocol.unstructured.FloodProtocol;
import org.p2psimulator.entity.protocol.messages.MessageTypes;
import org.p2psimulator.entity.protocol.messages.general.DownloadAnswerMessage;
import org.p2psimulator.entity.protocol.messages.general.DownloadRequestMessage;
import org.p2psimulator.entity.protocol.messages.general.QueryHitMessage;
import org.p2psimulator.entity.protocol.messages.gnutella.p2prep.P2PPoll;
import org.p2psimulator.entity.protocol.messages.gnutella.p2prep.P2PPollReply;
import org.p2psimulator.entity.protocol.messages.gnutella.p2prep.P2PTrueVote;
import org.p2psimulator.entity.protocol.messages.gnutella.p2prep.P2PTrueVoteReply;
import org.p2psimulator.entity.reputation.Reputation;
import org.p2psimulator.entity.reputation.ReputationRepository;
import org.p2psimulator.entity.reputation.ReputationRepositoryTypes;
import org.p2psimulator.entity.reputation.gnutellabased.GnutellaReputation;
import org.p2psimulator.exceptions.OverlayNetworkException;
import org.p2psimulator.exceptions.ReputationRepositoryException;
import org.p2psimulator.engine.Environment;
import org.p2psimulator.entity.overlaynetwork.FloodOverlay;
import org.p2psimulator.util.Quad;
import org.p2psimulator.util.Tuple;

public class P2PRepProtocol extends FloodProtocol {

    private static Random random = new Random();
    private Set<Id> pollreplyIdSet = new HashSet<Id>();
    private Quad<Id, Id, Double, BehaviorTypes> currentBestServent;

    public P2PRepProtocol(Environment enviroment) {
        super(enviroment);
    }

    @Override
    public synchronized void answareSpecificProtocolMessages(Peer peer,
            Message msg) {

        if (msg instanceof P2PPoll) {
            answarePoll(peer, (P2PPoll) msg);
        } else if (msg instanceof P2PTrueVote) {
            answareTrueVote(peer, (P2PTrueVote) msg);
        }
    }

    private void answarePoll(Peer p, P2PPoll msg) {

        DoubleId pollReplyId = new DoubleId(random.nextDouble());
        P2PPollReply reply = new P2PPollReply(p.getId(), msg.getOrigin(), msg.getTransactionId(), p.getBehavior().getBehaviorType());
        reply.setServentId(p.getId());

        reply.setPollReplyId(pollReplyId);
        reply.setTTL(1);
        reply.setHOP(0);
        reply.setTransactionId(msg.getTransactionId());

        // servent voting
        for (Id ratee : msg.getServent_ids()) {

            BehaviorTypes behaviorType = env.getPeerBank().getPeerMap().get(ratee).getBehavior().getBehaviorType();
            double rate = p.getBehavior().ratePeer(
                    p.getId(),
                    ratee,
                    behaviorType,
                    env.getReputationManager().getReputationRepository(
                    ReputationRepositoryTypes.REP_SERVENT_REPOSITORY));
            reply.getServentVotes().put(ratee, rate);
        }

        // dispatch
        env.getNetwork().sendMessage(reply);

        pollreplyIdSet.add(reply.getPollReplyId());
    }

    private void answareTrueVote(Peer p, P2PTrueVote msg) {

        Id reply = msg.getPollReplyId();

        if (!pollreplyIdSet.contains(reply)) {
            return;
        } else {
            P2PTrueVoteReply tvReply = new P2PTrueVoteReply(p.getId(), msg.getOrigin(), msg.getTransactionId(), p.getBehavior().getBehaviorType());

            tvReply.setServentId(p.getId());
            tvReply.setVote(true);
            tvReply.setVoteReplyId(msg.getPollReplyId());
            tvReply.setTTL(1);
            tvReply.setHOP(0);
            env.getNetwork().sendMessage(tvReply);
        }
    }

    @Override
    public String pollCycle(Peer p) {

        StringBuilder ret = new StringBuilder();
        List<Message> queryhits = p.getMessageStack().get(
                MessageTypes.QUERY_HIT_MESSAGE.getName());

        int qhSize = 0;
        short cnt = 0;
        if (queryhits != null && queryhits.size() > 0) {

            qhSize = queryhits.size();
            List<Id> serventsList = new ArrayList<Id> ();

            QueryHitMessage qh = null;
            for (Message msg : queryhits) {
                qh = (QueryHitMessage) msg;
                serventsList.add(qh.getServentId());
            }
            for (Id b : p.getBehavior().getBestKnownPeers(
                    p.getId(),
                    env.getReputationManager().getReputationRepository(
                    ReputationRepositoryTypes.REP_SERVENT_REPOSITORY),
                    env.getFloodOverlayNetwork())) {

                P2PPoll poll = new P2PPoll(p.getId(), b, currentTransactionId, p.getBehavior().getBehaviorType());
                poll.setHOP(0);
                poll.setTTL(7);
                poll.setServentIds(serventsList);
                env.getNetwork().sendMessage(poll);
                cnt++;
            }
        }
        ret.append(" [ QUERYHITS RECEBIDAS: " + qhSize + " ] ");
        ret.append(" [ POLL ENVIADOS: " + cnt + " ]");
        return ret.toString();
    }

    @Override
    public String trueVoteCycle(Peer p) {

        StringBuilder ret = new StringBuilder();
        List<Message> pollreplys = p.getMessageStack().get(
                MessageTypes.P2P_POLLREPLY.getName());
        int cnt = 0;
        Object [] oArray = pollreplys.toArray();
        for (Object msg : oArray) {
            P2PPollReply reply = (P2PPollReply) msg;
            P2PTrueVote tV = new P2PTrueVote(p.getId(), reply.getServentId(),
                    currentTransactionId, p.getBehavior().getBehaviorType());
            tV.setPollReplyId(reply.getPollReplyId());
            tV.setTTL(1);
            tV.setHOP(0);
            env.getNetwork().sendMessage(tV);
            cnt++;
        }

        ret.append(" [ POLLREPLYS RECEBIDAS: " + cnt + " ] ");
        return ret.toString();
    }

    @Override
    public String downloadRequestCycle(Peer p) {

        StringBuilder ret = new StringBuilder();

        // 5 - Faz a confirmao dos votos recebidos
        List<Message> pollReplys = p.getMessageStack().get(
                MessageTypes.P2P_POLLREPLY.getName());
        List<Message> trueVoteReplys = p.getMessageStack().get(
                MessageTypes.P2P_TRUEVOTEREPLY.getName());

        List<Message> queryHitList = p.getMessageStack().get(
                MessageTypes.QUERY_HIT_MESSAGE.getName());

        // 6 - Avaliao dos votos recebidos
        currentBestServent = analyzePollReply(p, pollReplys);

        int tVReceived = 0;
        int downloadRequest = 0;
        if (queryHitList != null && queryHitList.size() > 0) {
            QueryHitMessage qh = (QueryHitMessage) queryHitList.get(0);
            if (currentBestServent == null) {
                currentBestServent = new Quad<Id, Id, Double, BehaviorTypes>(qh.getServentId(), qh.getServentId(), 0d, qh.getOriginBehavior());
            }

            if (trueVoteReplys != null) {
                tVReceived = trueVoteReplys.size();
                pollReplys = checkPollReplys(pollReplys, trueVoteReplys);
            }

            // 7 - Inicia o download do recurso;

            DownloadRequestMessage request = new DownloadRequestMessage(p.getId(), currentBestServent.getB(), currentTransactionId, p.getBehavior().getBehaviorType());
            request.setFileId(qh.getFileId());
            request.setTTL(1);
            request.setHOP(0);
            request.setServentId(p.getId());
            env.getNetwork().sendMessage(request);
            downloadRequest++;
        }
        ret.append(" [ TRUEVOTES RECEBIDAS: " + tVReceived + " ] ");
        ret.append(" [ DOWNLOAD REQUESTS: " + downloadRequest + " ] ");
        return ret.toString();
    }

    @Override
    public String rateCycle(Peer peer) throws ReputationRepositoryException {

        StringBuilder ret = new StringBuilder();

        List<Message> downloadAnswareList = peer.getMessageStack().get(
                MessageTypes.DOWNLOAD_ANSWARE_MESSAGE.getName());

        int totalDownlaodReplys = 0;
        double rating = 0;
        int result = 0;

        if (currentBestServent != null) {

            if (downloadAnswareList != null && downloadAnswareList.size() > 0) {

                totalDownlaodReplys = downloadAnswareList.size();
                DownloadAnswerMessage reply = (DownloadAnswerMessage) downloadAnswareList.get(0);

                DoubleId file = reply.getFile();

                if (reply.isCorrupted()) {
                    result = -1;
                } else {
                    if (reply.isNeutro()) {
                        result = -2;
                    } else {
                        result = 1;
                    }
                    try {
                        env.getFileSystem().getMatrix().get(file).add(
                                peer.getId());
                    } catch (Exception ex) {
                    }

                }

                rating = peer.getBehavior().rateTransaction(
                        peer.getId(),
                        reply.getServentId(),
                        result,
                        reply.getOriginBehavior(),
                        env.getReputationManager().getReputationRepository(
                        ReputationRepositoryTypes.REP_SERVENT_REPOSITORY));
            } else {
                result = 0;

                rating = peer.getBehavior().rateTransaction(
                        peer.getId(),
                        currentBestServent.getA(),
                        result,
                        currentBestServent.getD(),
                        env.getReputationManager().getReputationRepository(
                        ReputationRepositoryTypes.REP_SERVENT_REPOSITORY));
            }

            synchronized (currentBestServent) {

                if (peer.getBehavior().rate()) {
                    /**
                     * Atualiza reputa��o de servent
                     */
                    ReputationRepository serventRepository = env.getReputationManager().getReputationRepository(
                            ReputationRepositoryTypes.REP_SERVENT_REPOSITORY);

                    Reputation reputation = serventRepository.getLocalReputation(peer.getId(),
                            currentBestServent.getB());

                    if (reputation == null) {
                        reputation = new GnutellaReputation(0, 0, peer.getId(),
                                currentBestServent.getB());

                        reputation.setNumminus(0);
                        reputation.setNumplus(0);
                        serventRepository.getReputationMap().get(peer.getId()).put(currentBestServent.getB(), reputation);

                    }
                    if (rating > 0) {
                        reputation.incPlus();
                    } else if (rating < 0) {
                        reputation.incMinus();
                    } else if (rating == 0) {
                        reputation.incPlus();
                        reputation.incMinus();
                    }
                    /**
                     * Atualiza credibilidade do servent
                     */
                    ReputationRepository credibilityRepository = env.getReputationManager().getReputationRepository(
                            ReputationRepositoryTypes.TRUST_REPOSITORY);

                    Reputation credibility = credibilityRepository.getLocalReputation(peer.getId(),
                            currentBestServent.getA());

                    if (credibility == null) {
                        credibility = new GnutellaReputation(0, 0, peer.getId(),
                                currentBestServent.getA());
                        credibilityRepository.getReputationMap().get(
                                peer.getId()).put(currentBestServent.getA(),
                                credibility);
                    }
                    if (result > 0) {

                        if (currentBestServent.getC() > 0) {
                            credibility.incPlus();
                        } else if (currentBestServent.getC() < 0) {
                            credibility.incMinus();
                        } else if (currentBestServent.getC() == 0) {
                            credibility.incMinus();
                        }
                    } else if (result < 0) {
                        if (currentBestServent.getC() > 0
                                || currentBestServent.getC() == 0) {
                            credibility.incMinus();
                        } else if (currentBestServent.getC() < 0) {
                            credibility.incPlus();
                        }
                    } else if (result == 0) {
                        if (currentBestServent.getC() == 0) {
                            credibility.incPlus();
                        } else {
                            credibility.incMinus();
                        }
                    }
                }

                ret.append(" [ DOWNLOADS RECEBIDOS: " + totalDownlaodReplys
                        + " ]");
                ret.append(" [ RATING: (  [" + peer.getId() + "] "
                        + peer.getBehavior().getBehaviorType().getTypeName()
                        + " -> " + currentBestServent.getD().getTypeName()
                        + "  [" + currentBestServent.getB() + "]) " + rating
                        + " ]");
            }
        } else {
            ret.append(" [ DOWNLOADS RECEBIDOS: " + totalDownlaodReplys + " ]");
            ret.append(" [ RATING: (  [" + peer.getId() + "] "
                    + peer.getBehavior().getBehaviorType().getTypeName()
                    + " ->   [" + null + "]) " + rating + " ]");
        }
        pollreplyIdSet.clear();
        currentBestServent = null;
        peer.getProtocol().setTransactionResult(result);
        return ret.toString();
    }

    /**
     * Retorna os ids do peer e recurso mais bem votados respecitivamente
     *
     * @param p
     * @param pollreplys
     * @return Triple (voter, voted, vote)
     */
    private Quad<Id, Id, Double, BehaviorTypes> analyzePollReply(Peer p,
            List<Message> pollreplys) {

        double maxServentVote = Integer.MIN_VALUE;
        Id bestServentKey = null;
        Id bestVoter = null;
        BehaviorTypes bestType = null;

        ReputationRepository credibilityRepository = env.getReputationManager().getReputationRepository(
                ReputationRepositoryTypes.TRUST_REPOSITORY);
        ReputationRepository reputationRepository = env.getReputationManager().getReputationRepository(
                ReputationRepositoryTypes.REP_SERVENT_REPOSITORY);

        if (pollreplys != null && pollreplys.size() > 0) {
            try {
                for (Message msg : pollreplys) {

                    P2PPollReply pr = (P2PPollReply) msg;
                    Tuple<Id, Double> tuple = pr.getBestServentVote();

                    Reputation credibility = credibilityRepository.getLocalReputation(p.getId(), pr.getServentId());
                    Reputation reputation = reputationRepository.getLocalReputation(p.getId(), pr.getServentId());
                    double credValue = 0;
                    double repValue = 0;

                    if (credibility != null) {
                        credValue = credibility.getAverageReputation();
                    }
                    if (reputation != null) {
                        repValue = reputation.getAverageReputation();
                    }

                    double rate = (tuple.getB() + (credValue + repValue) / 2) / 2;
                    if (rate > maxServentVote) {
                        maxServentVote = tuple.getB();
                        bestServentKey = tuple.getA();
                        bestVoter = pr.getServentId();
                        bestType = env.getPeerBank().getPeerMap().get(tuple.getA()).getBehavior().getBehaviorType();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            return null;
        }
        if (bestType == null) {
            return null;
        }

        return new Quad<Id, Id, Double, BehaviorTypes>(bestVoter,
                bestServentKey, maxServentVote, bestType);
    }

    /**
     * Compara os pollreplys recebidos com os truvotes recebidos. Retorna todos
     * os pollReplys que foram confirmados por trueVotes em um vetor
     *
     * @param pollReplys
     * @param trueVoteReplys
     * @return
     */
    private List<Message> checkPollReplys(List<Message> pollReplys,
            List<Message> trueVoteReplys) {

        List<Message> aux = new ArrayList<Message> ();

        Object [] voteReplys = trueVoteReplys.toArray();
        for (Object msg : voteReplys) {
            P2PTrueVoteReply tvr = (P2PTrueVoteReply) msg;
            for (Message poll : pollReplys) {
                P2PPollReply pr = (P2PPollReply) poll;

                if (pr.getPollReplyId().equals(tvr.getVoteReplyId())) {
                    aux.add(pr);
                }
            }
        }

        return aux;
    }

      @Override
    public String joinNetworkRequestCycle(Peer p) {

        StringBuilder ret = new StringBuilder(" [ P2P JOIN REQUEST CYCLE - DONE ] ");

        FloodOverlay overlay = (FloodOverlay) env.getFloodOverlayNetwork();
        try {
            overlay.joinNetwork(p.getId());
            p.setOnline(true);
        } catch (OverlayNetworkException ex) {
            Logger.getLogger(P2PRepProtocol.class.getName()).log(Level.SEVERE, null, ex);
        }
        return ret.toString();
    }
    @Override
    public String joinNetworkResponseCycle(Peer p) {

         StringBuilder ret = new StringBuilder(" [ P2P JOIN RESPONSE CYCLE - DONE ] ");
        return ret.toString();
    }
    @Override
    public String leaveNetworkCycle(Peer p) {

        String ret = super.leaveNetworkCycle(p);
        p.setOnline(false);

        return ret;
    }
}
